6.3 组合与继承的结合

许多时候都要求将组合与继承两种技术结合起来使用。下面这个例子展示了如何同时采用继承与组合技术,从而创建一个更复杂的类,同时进行必要的构造器初始化工作:

  1. //: PlaceSetting.java
  2. // Combining composition & inheritance
  3. class Plate {
  4. Plate(int i) {
  5. System.out.println("Plate constructor");
  6. }
  7. }
  8. class DinnerPlate extends Plate {
  9. DinnerPlate(int i) {
  10. super(i);
  11. System.out.println(
  12. "DinnerPlate constructor");
  13. }
  14. }
  15. class Utensil {
  16. Utensil(int i) {
  17. System.out.println("Utensil constructor");
  18. }
  19. }
  20. class Spoon extends Utensil {
  21. Spoon(int i) {
  22. super(i);
  23. System.out.println("Spoon constructor");
  24. }
  25. }
  26. class Fork extends Utensil {
  27. Fork(int i) {
  28. super(i);
  29. System.out.println("Fork constructor");
  30. }
  31. }
  32. class Knife extends Utensil {
  33. Knife(int i) {
  34. super(i);
  35. System.out.println("Knife constructor");
  36. }
  37. }
  38. // A cultural way of doing something:
  39. class Custom {
  40. Custom(int i) {
  41. System.out.println("Custom constructor");
  42. }
  43. }
  44. public class PlaceSetting extends Custom {
  45. Spoon sp;
  46. Fork frk;
  47. Knife kn;
  48. DinnerPlate pl;
  49. PlaceSetting(int i) {
  50. super(i + 1);
  51. sp = new Spoon(i + 2);
  52. frk = new Fork(i + 3);
  53. kn = new Knife(i + 4);
  54. pl = new DinnerPlate(i + 5);
  55. System.out.println(
  56. "PlaceSetting constructor");
  57. }
  58. public static void main(String[] args) {
  59. PlaceSetting x = new PlaceSetting(9);
  60. }
  61. } ///:~

尽管编译器会强迫我们对基类进行初始化,并要求我们在构造器最开头做这一工作,但它并不会监视我们是否正确初始化了成员对象。所以对此必须特别加以留意。

6.3.1 确保正确的清除

Java不具备象C++的“析构器”那样的概念。在C++中,一旦析构(清除)一个对象,就会自动调用析构器方法。之所以将其省略,大概是由于在Java中只需简单地忘记对象,不需强行析构它们。垃圾收集器会在必要的时候自动回收内存。

垃圾收集器大多数时候都能很好地工作,但在某些情况下,我们的类可能在自己的存在时期采取一些行动,而这些行动要求必须进行明确的清除工作。正如第4章已经指出的那样,我们并不知道垃圾收集器什么时候才会显身,或者说不知它何时会调用。所以一旦希望为一个类清除什么东西,必须写一个特别的方法,明确、专门地来做这件事情。同时,还要让客户程序员知道他们必须调用这个方法。而在所有这一切的后面,就如第9章(异常控制)要详细解释的那样,必须将这样的清除代码置于一个finally从句中,从而防范任何可能出现的异常事件。

下面介绍的是一个计算机辅助设计系统的例子,它能在屏幕上描绘图形:

  1. //: CADSystem.java
  2. // Ensuring proper cleanup
  3. import java.util.*;
  4. class Shape {
  5. Shape(int i) {
  6. System.out.println("Shape constructor");
  7. }
  8. void cleanup() {
  9. System.out.println("Shape cleanup");
  10. }
  11. }
  12. class Circle extends Shape {
  13. Circle(int i) {
  14. super(i);
  15. System.out.println("Drawing a Circle");
  16. }
  17. void cleanup() {
  18. System.out.println("Erasing a Circle");
  19. super.cleanup();
  20. }
  21. }
  22. class Triangle extends Shape {
  23. Triangle(int i) {
  24. super(i);
  25. System.out.println("Drawing a Triangle");
  26. }
  27. void cleanup() {
  28. System.out.println("Erasing a Triangle");
  29. super.cleanup();
  30. }
  31. }
  32. class Line extends Shape {
  33. private int start, end;
  34. Line(int start, int end) {
  35. super(start);
  36. this.start = start;
  37. this.end = end;
  38. System.out.println("Drawing a Line: " +
  39. start + ", " + end);
  40. }
  41. void cleanup() {
  42. System.out.println("Erasing a Line: " +
  43. start + ", " + end);
  44. super.cleanup();
  45. }
  46. }
  47. public class CADSystem extends Shape {
  48. private Circle c;
  49. private Triangle t;
  50. private Line[] lines = new Line[10];
  51. CADSystem(int i) {
  52. super(i + 1);
  53. for(int j = 0; j < 10; j++)
  54. lines[j] = new Line(j, j*j);
  55. c = new Circle(1);
  56. t = new Triangle(1);
  57. System.out.println("Combined constructor");
  58. }
  59. void cleanup() {
  60. System.out.println("CADSystem.cleanup()");
  61. t.cleanup();
  62. c.cleanup();
  63. for(int i = 0; i < lines.length; i++)
  64. lines[i].cleanup();
  65. super.cleanup();
  66. }
  67. public static void main(String[] args) {
  68. CADSystem x = new CADSystem(47);
  69. try {
  70. // Code and exception handling...
  71. } finally {
  72. x.cleanup();
  73. }
  74. }
  75. } ///:~

这个系统中的所有东西都属于某种Shape(几何形状)。Shape本身是一种Object(对象),因为它是从根类明确继承的。每个类都重新定义了Shapecleanup()方法,同时还要用super调用那个方法的基类版本。尽管对象存在期间调用的所有方法都可负责做一些要求清除的工作,但对于特定的Shape类——Circle(圆)、Triangle(三角形)以及Line(直线),它们都拥有自己的构造器,能完成“作图”(draw)任务。每个类都有它们自己的cleanup()方法,用于将非内存的东西恢复回对象存在之前的景象。

main()中,可看到两个新关键字:tryfinally。我们要到第9章才会向大家正式引荐它们。其中,try关键字指出后面跟随的块(由花括号定界)是一个“警戒区”。也就是说,它会受到特别的待遇。其中一种待遇就是:该警戒区后面跟随的finally从句的代码肯定会得以执行——不管try块到底存不存在(通过异常控制技术,try块可有多种不寻常的应用)。在这里,finally从句的意思是“总是为x调用cleanup(),无论会发生什么事情”。这些关键字将在第9章进行全面、完整的解释。

在自己的清除方法中,必须注意对基类以及成员对象清除方法的调用顺序——假若一个子对象要以另一个为基础。通常,应采取与C++编译器对它的“析构器”采取的同样的形式:首先完成与类有关的所有特殊工作(可能要求基类元素仍然可见),然后调用基类清除方法,就象这儿演示的那样。

许多情况下,清除可能并不是个问题;只需让垃圾收集器尽它的职责即可。但一旦必须由自己明确清除,就必须特别谨慎,并要求周全的考虑。

(1) 垃圾收集的顺序

不能指望自己能确切知道何时会开始垃圾收集。垃圾收集器可能永远不会得到调用。即使得到调用,它也可能以自己愿意的任何顺序回收对象。除此以外,Java 1.0实现的垃圾收集器机制通常不会调用finalize()方法。除内存的回收以外,其他任何东西都最好不要依赖垃圾收集器进行回收。若想明确地清除什么,请制作自己的清除方法,而且不要依赖finalize()。然而正如以前指出的那样,可强迫Java1.1调用所有收尾模块(Finalizer)。

6.3.2 名字的隐藏

只有C++程序员可能才会惊讶于名字的隐藏,因为它的工作原理与在C++里是完全不同的。如果Java基类有一个方法名被“重载”使用多次,在派生类里对那个方法名的重新定义就不会隐藏任何基类的版本。所以无论方法在这一级还是在一个基类中定义,重载都会生效:

  1. //: Hide.java
  2. // Overloading a base-class method name
  3. // in a derived class does not hide the
  4. // base-class versions
  5. class Homer {
  6. char doh(char c) {
  7. System.out.println("doh(char)");
  8. return 'd';
  9. }
  10. float doh(float f) {
  11. System.out.println("doh(float)");
  12. return 1.0f;
  13. }
  14. }
  15. class Milhouse {}
  16. class Bart extends Homer {
  17. void doh(Milhouse m) {}
  18. }
  19. class Hide {
  20. public static void main(String[] args) {
  21. Bart b = new Bart();
  22. b.doh(1); // doh(float) used
  23. b.doh('x');
  24. b.doh(1.0f);
  25. b.doh(new Milhouse());
  26. }
  27. } ///:~

正如下一章会讲到的那样,很少会用与基类里完全一致的签名和返回类型来覆盖同名的方法,否则会使人感到迷惑(这正是C++不允许那样做的原因,所以能够防止产生一些不必要的错误)。